﻿// Copyright (c)  YISH. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Text;
using NSQL.DateFormats;
using NSQL.Expressions;
using NSQL.Expressions.Functions;

namespace NSQL.Generator
{
    public class OracleGenerator : BaseGenerator, IExpressionVisitor<StringBuilder>
    {  
        public static SqlGeneratorSettings DefaultSettings => new SqlGeneratorSettings
        {
            LetterCase = LetterCase.UpperCase,
            DatetimeFormatter = OracleDateFormatter.Instance
        };
        public static readonly ISqlGenerator Instance = new OracleGenerator(DefaultSettings);

        private readonly OracleKeyWords _;

        StringBuilder IFunctionVisitor<StringBuilder>.Visit(ILogFunction expression)
        {
            var builder = new StringBuilder();
            builder.Append(_.Log);
            builder.Append('(');

            if (!(expression.Base is null))
            {
                builder.Append(expression.Base.Accept(this));

                builder.Append(", ");
            }

            builder.Append(expression.Number.Accept(this));
            builder.Append(')');
            
            return builder;
        }
        
        StringBuilder IFunctionVisitor<StringBuilder>.Visit(IMedianFunction expression)
        {
            var builder = new StringBuilder();
            builder.Append(_.Median);
            builder.Append('(').Append(expression.Expr.Accept(this)).Append(')');
            return builder;
        }
        
        StringBuilder IFunctionVisitor<StringBuilder>.Visit(ICastFunction castFunction)
        {
            var targetType = castFunction.TargetType;
            if (string.Equals(targetType, "time", StringComparison.OrdinalIgnoreCase))
            {
                throw new NotSupportedException();
            }
            var builder = new StringBuilder();
            if (string.Equals(targetType, "text", StringComparison.OrdinalIgnoreCase))
            {
                builder.Append(_.To).Append('_').Append(_.Char)
                    .Append('(')
                    .Append(castFunction.Expr.Accept(this))
                    .Append(')');
                return builder;
            }

            if (string.Equals(targetType, "date", StringComparison.OrdinalIgnoreCase))
            {
                builder.Append(_.To).Append('_').Append(_.Date)
                    .Append('(')
                    .Append(castFunction.Expr.Accept(this))
                    .Append(", ")
                    .Append('\'').Append(Settings.DatetimeFormatter.Format("yyyy-mm-dd")).Append('\'')
                    .Append(')');
                return builder;
            }

            if (string.Equals(targetType, "datetime", StringComparison.OrdinalIgnoreCase) ||
                string.Equals(targetType, "timestamp", StringComparison.OrdinalIgnoreCase))
            {
                builder.Append(_.To).Append('_').Append(_.Timestamp)
                    .Append('(')
                    .Append(castFunction.Expr.Accept(this))
                    .Append(", ")
                    .Append('\'').Append(Settings.DatetimeFormatter.Format("yyyy-mm-dd hh24:mi:ss")).Append('\'')
                    .Append(')');
                return builder;
            }
            builder.Append(_.Cast);
            builder.Append('(');
            builder.Append(castFunction.Expr.Accept(this));
            builder.Append(' ').Append(_.As).Append(' ');
            builder.Append(targetType);
            builder.Append(')');
            return builder;
        }

        protected override StringBuilder FormatPaging(StringBuilder statement, int skip, int limit)
        {
            if (Settings.DatabaseVersion != null && Settings.DatabaseVersion.Major <= 11)
            {
                if (skip <= 0)
                {
                    if (limit <= 0) return statement;
                    var builder = new StringBuilder();

                    builder.Append(_.Select);
                    builder.Append(' ').Append('*').Append(' ');
                    builder.Append(_.From);
                    builder.Append(' ').Append('(').Append(statement).Append(')').Append(' ');
                    builder.Append(_.Where);
                    builder.Append(' ').Append(_.RowNum).Append(' ').Append("<=").Append(' ').Append(limit);
                    return builder;
                }
                else
                {
                    const string pagingAlias1 = "&1";
                    const string pagingAlias2 = "&2";
                    const string rowNum = ".";
                    var builder = new StringBuilder();
                    builder.Append(_.Select);
                    builder.Append(' ')
                        .Append(Settings.BeginDelimiter).Append(pagingAlias1).Append(Settings.EndDelimiter).Append(".*");
                    builder.Append(' ').Append(_.From);
                    builder.Append(' ')
                        .Append('(')
                        .Append(_.Select).Append(' ').Append(_.RowNum).Append(' ')
                        .Append(Settings.BeginDelimiter).Append(rowNum).Append(Settings.EndDelimiter)
                        .Append(", ")
                        .Append(Settings.BeginDelimiter).Append(pagingAlias2).Append(Settings.EndDelimiter).Append(".*")
                        .Append(' ').Append(_.From).Append(' ')
                        .Append('(').Append(statement)
                        .Append(')').Append(' ').Append(Settings.BeginDelimiter).Append(pagingAlias2).Append(Settings.EndDelimiter)
                        .Append(')').Append(' ').Append(Settings.BeginDelimiter).Append(pagingAlias1).Append(Settings.EndDelimiter);

                    builder.Append(' ').Append(_.Where).Append(' ');

                    builder.Append(Settings.BeginDelimiter).Append(rowNum).Append(Settings.EndDelimiter)
                        .Append(" > ").Append(skip);

                    if (limit <= 0) return builder;
                    builder.Append(' ').Append(_.And).Append(' ')
                        .Append(Settings.BeginDelimiter).Append(rowNum).Append(Settings.EndDelimiter);

                    builder.Append(" <= ");
                    builder.Append(limit + skip);
                    return builder;
                }

            }
            if (skip > 0)
            {
                statement.Append(' ').Append(_.Offset);
                statement.Append(' ').Append(skip);
                statement.Append(' ').Append(skip > 1 ? _.Rows : _.Row);
            }

            if (limit <= 0) return statement;

            statement.Append(' ').Append(_.Fetch);
            statement.Append(' ').Append(_.First);
            statement.Append(' ').Append(limit);
            statement.Append(' ').Append(limit > 1 ? _.Rows : _.Row);
            statement.Append(' ').Append(_.Only);
            
            return statement;
        }

        protected override StringBuilder FormatSelect(StringBuilder select, StringBuilder from, StringBuilder where, StringBuilder group,
            StringBuilder order, StringBuilder having)
        {
            if (!(from?.Length > 0))
            {
                from = new StringBuilder(_.Dual);
            }
            return base.FormatSelect(select, from, where, group, order, having);
        }
        
        StringBuilder IFunctionVisitor<StringBuilder>.Visit(IDecodeFunction expression)
        {
            var builder = new StringBuilder(_.Decode);
            builder.Append('(').Append(expression.Items.Accept(this)).Append(')');
            return builder;
        }
        
        StringBuilder IFunctionVisitor<StringBuilder>.Visit(IDiffFunction expression)
        {
            var builder = new StringBuilder();
            if (expression.Type != ExtractType.Day)
            {
                builder.Append('(');
            }

            builder.Append(expression.End.Accept(this))
                .Append(" - ")
                .Append(expression.Start.Accept(this));

            if (expression.Type != ExtractType.Day)
            {
                builder.Append(')');
            }

            switch (expression.Type)
            {
                case ExtractType.Year:
                    builder.Append(" / ").Append(365);
                    break;
                case ExtractType.Month:
                    builder.Append(" / ").Append(30);
                    break;
                case ExtractType.Week:
                    builder.Append(" / ").Append(7);
                    break;
                case ExtractType.Day:
                    break;
                case ExtractType.Hour:
                    builder.Append(" * ").Append(24);
                    break;
                case ExtractType.Minute:
                    builder.Append(" * ").Append(24 * 60);
                    break;
                case ExtractType.Second:
                    builder.Append(" * ").Append(24 * 60 * 60);
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
            return builder;
        }
        
        StringBuilder IFunctionVisitor<StringBuilder>.Visit(ILeftFunction leftFunction)
        {
            return new SubStringFunction(leftFunction.Text, (IntegerExpression) 1, leftFunction.Length).Accept(this);
        }
        StringBuilder IFunctionVisitor<StringBuilder>.Visit(IRightFunction rightFunction)
        {
            return new SubStringFunction(rightFunction.Text, new LengthFunction(rightFunction.Text) - rightFunction.Length + 1, rightFunction.Length).Accept(this);
        }
        
        StringBuilder IFunctionVisitor<StringBuilder>.Visit(ITrimFunction trimFunction)
        {
            var  builder =  new StringBuilder();

            if (trimFunction.Type == TrimType.Both)
            {
                var hasTrimExpr = !(trimFunction.TrimExpr is null);
                if (hasTrimExpr)
                {
                    builder.Append(_.Trim);
                }
                else
                {
                    switch (trimFunction.Type)
                    {
                        case TrimType.Both:
                            builder.Append(_.Trim);
                            break;
                        case TrimType.Leading:
                            builder.Append(_.TrimStart);
                            break;
                        case TrimType.Trailing:
                            builder.Append(_.TrimEnd);
                            break;
                        default:
                            throw new ArgumentOutOfRangeException();
                    }
                }
            
                builder.Append('(');

                if (hasTrimExpr)
                {
                    switch (trimFunction.Type)
                    {
                        case TrimType.Both:
                            builder.Append(_.Both);
                            break;
                        case TrimType.Leading:
                            builder.Append(_.Leading);
                            break;
                        case TrimType.Trailing:
                            builder.Append(_.Trailing);
                            break;
                        default:
                            throw new ArgumentOutOfRangeException();
                    }
                
                
                    builder.Append(' ').Append(trimFunction.TrimExpr.Accept(this));
                    builder.Append(' ').Append(_.From).Append(' ');
                }

                builder.Append(trimFunction.Expr.Accept(this));
                builder.Append(')');
            }
            else
            {
                switch (trimFunction.Type)
                {
                    case TrimType.Both:
                        builder.Append(_.Trim);
                        break;
                    case TrimType.Leading:
                        builder.Append(_.TrimStart);
                        break;
                    case TrimType.Trailing:
                        builder.Append(_.TrimEnd);
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            
                builder.Append('(');
                builder.Append(trimFunction.Expr.Accept(this));
                if (trimFunction.TrimExpr != null)
                {
                    builder.Append(", ").Append(trimFunction.TrimExpr.Accept(this));
                }
                builder.Append(')');
            }

            return builder;
        }
        
        StringBuilder IFunctionVisitor<StringBuilder>.Visit(INvlFunction nvlFunction)
        {
            var builder = new StringBuilder();
            var hasThen = !(nvlFunction.Then is null);

            builder.Append(hasThen ? _.Nvl2 : _.Nvl);
            
            builder.Append('(');
            builder.Append(nvlFunction.Expr.Accept(this));
            if (hasThen)
            {
                builder.Append(", ").Append(nvlFunction.Then.Accept(this));
            }
            builder.Append(", ").Append(nvlFunction.Default.Accept(this));
            builder.Append(')');
            return builder;
        }

        public OracleGenerator(Func<SqlGeneratorSettings, SqlGeneratorSettings> setupAction)
            : this(setupAction(DefaultSettings))
        {
        }
        public OracleGenerator(SqlGeneratorSettings settings) : this(settings, new OracleKeyWords(settings.LetterCase))
        {
        }

        private OracleGenerator(SqlGeneratorSettings settings, OracleKeyWords keyWords) : base(settings, keyWords)
        {
            _ = keyWords;
        }
        
        protected class OracleKeyWords : KeyWords
        {
            public OracleKeyWords(LetterCase letterCase) : base(letterCase)
            {
            }
            
            public string Offset => FormatLetter(nameof(Offset));
            public string Row => FormatLetter(nameof(Row));
            public string Rows => FormatLetter(nameof(Rows));
            public string Fetch => FormatLetter(nameof(Fetch));
            public string First => FormatLetter(nameof(First));
            public string Only => FormatLetter(nameof(Only));
            public string Nvl2 => FormatLetter(nameof(Nvl2));
            public string Dual => FormatLetter(nameof(Dual));
            public string Decode => FormatLetter(nameof(Decode));
            public string RowNum => FormatLetter(nameof(RowNum));
            public override string PadLeft => FormatLetter("LPad");
            public override string PadRight => FormatLetter("RPad");
            public override string TrimStart => FormatLetter("LTrim");
            public override string TrimEnd => FormatLetter("RTrim");
            public override string Now => FormatLetter("SysTimestamp");
        }
    }
}